#pragma once

#include <memory>
#include <stack>
#include <stdexcept>
#include <type_traits>
#include <zenoui/widgets/EmptyWidget.hpp>
#include <zenoui/Property.hpp>
#include <zenoui/Widget.hpp>

namespace zenoui {

struct DOMBuilder { // convinent helper class for quickly building widget hierachy directly in code
    std::stack<zenox::shared_ptr<Widget>> m_stack;

    DOMBuilder() : DOMBuilder(zenox::make_shared<EmptyWidget>()) {}

    DOMBuilder(DOMBuilder &&) = default; // allow move, disable copy
    DOMBuilder &operator=(DOMBuilder &&) = default;

    explicit DOMBuilder(zenox::shared_ptr<Widget> root_wig) {
        m_stack.push(std::move(root_wig));
    }

    zenox::shared_ptr<Widget> endDOM() {
        if (m_stack.size() != 1) [[unlikely]] {
            throw std::invalid_argument("widget() and endw() not evenly paired");
        }
        return std::move(m_stack.top());
    }

    template <class DerivedWidget>
    DOMBuilder &widget() {
        static_assert(std::is_base_of_v<Widget, DerivedWidget>);
        auto ptr = zenox::make_shared<DerivedWidget>();
        m_stack.push(std::move(ptr));
        return *this;
    }

    DOMBuilder &layout(LayoutMode layout) {
        auto &sp = m_stack.top()->m_shape;
        sp.layout = layout;
        return *this;
    }

    DOMBuilder &shape(Shape const &shape) {
        auto &sp = m_stack.top()->m_shape;
        sp = shape;
        return *this;
    }

    DOMBuilder &size(glm::vec2 wh) {
        return size(wh.x, wh.y);
    }

    DOMBuilder &size(UnitedSize width, UnitedSize height) {
        auto &sp = m_stack.top()->m_shape;
        sp.size[0] = width;
        sp.size[1] = height;
        return *this;
    }

    DOMBuilder &width(UnitedSize width) {
        auto &sp = m_stack.top()->m_shape;
        sp.size[0] = width;
        return *this;
    }

    DOMBuilder &height(UnitedSize height) {
        auto &sp = m_stack.top()->m_shape;
        sp.size[1] = height;
        return *this;
    }

    DOMBuilder &padding(UnitedSize padding) {
        auto &sp = m_stack.top()->m_shape;
        sp.paddings[0][0] = padding;
        sp.paddings[0][1] = padding;
        sp.paddings[1][0] = padding;
        sp.paddings[1][1] = padding;
        return *this;
    }

    DOMBuilder &padding_left(UnitedSize padding) {
        auto &sp = m_stack.top()->m_shape;
        sp.paddings[0][0] = padding;
        return *this;
    }

    DOMBuilder &padding_right(UnitedSize padding) {
        auto &sp = m_stack.top()->m_shape;
        sp.paddings[0][1] = padding;
        return *this;
    }

    DOMBuilder &padding_top(UnitedSize padding) {
        auto &sp = m_stack.top()->m_shape;
        sp.paddings[1][0] = padding;
        return *this;
    }

    DOMBuilder &padding_bottom(UnitedSize padding) {
        auto &sp = m_stack.top()->m_shape;
        sp.paddings[1][1] = padding;
        return *this;
    }

    DOMBuilder &gview_translation(float x, float y) {
        return gview_translation({x, y});
    }

    DOMBuilder &gview_translation(glm::vec2 translation) {
        auto &sp = m_stack.top()->m_shape;
        sp.transform.translation = translation;
        return *this;
    }

    DOMBuilder &gview_scaling(float scaling) {
        auto &sp = m_stack.top()->m_shape;
        sp.transform.scaling = scaling;
        return *this;
    }

    DOMBuilder &gitem_position(float x, float y) {
        return gitem_position({x, y});
    }

    DOMBuilder &gitem_position(glm::vec2 position) {
        auto &sp = m_stack.top()->m_shape;
        sp.position = position;
        return *this;
    }

    DOMBuilder &margin(UnitedSize margin) {
        auto &sp = m_stack.top()->m_shape;
        sp.margins[0][0] = margin;
        sp.margins[0][1] = margin;
        sp.margins[1][0] = margin;
        sp.margins[1][1] = margin;
        return *this;
    }

    DOMBuilder &margin_left(UnitedSize margin) {
        auto &sp = m_stack.top()->m_shape;
        sp.margins[0][0] = margin;
        return *this;
    }

    DOMBuilder &margin_right(UnitedSize margin) {
        auto &sp = m_stack.top()->m_shape;
        sp.margins[0][1] = margin;
        return *this;
    }

    DOMBuilder &margin_top(UnitedSize margin) {
        auto &sp = m_stack.top()->m_shape;
        sp.margins[1][0] = margin;
        return *this;
    }

    DOMBuilder &margin_bottom(UnitedSize margin) {
        auto &sp = m_stack.top()->m_shape;
        sp.margins[1][1] = margin;
        return *this;
    }

    template <class DerivedProperty>
    DOMBuilder &prop(typename DerivedProperty::value_type value) {
        static_assert(std::is_base_of_v<Property, DerivedProperty>);
        DerivedProperty::set(m_stack.top().get(), std::move(value));
        return *this;
    }

    DOMBuilder &endw() {
        auto ptr = std::move(m_stack.top());
        ptr->update_rect();
        m_stack.pop();
        m_stack.top()->add_child(std::move(ptr));
        if (m_stack.empty()) throw std::invalid_argument("extra endw() without no paired widget()");
        return *this;
    }
};

/* DOMBuilder usage example (see src/main.cpp):
    auto root = DOMBuilder()
        .widget<ButtonWidget>()
            .widget<LabelWidget>()
                .prop<TextProperty>("Pause")
                .margin_left(20_pc)  // 20%
                .margin_right(20_px) // 20px
            .endw()
        .endw()
    .endDOM();
*/

/* struct DOMMocker : DOMBuilder { */
/*     // create a fake 'unique' pointer from a non-owning raw pointer, actually being leaked (release) later */
/*     explicit DOMMocker(Widget *root_wig) : DOMBuilder(std::unique_ptr<Widget>(root_wig)) { */
/*     } */
/*  */
/*     ~DOMMocker() { */
/*         if (!DOMBuilder::m_stack.empty()) */
/*             (void)DOMBuilder::m_stack.top().release(); // leak memory purposely, as we didn't own root_wig at the first place */
/*     } */
/*  */
/*     Widget *endDOM() { */
/*         return DOMBuilder::endDOM().release(); // leak memory purposely, as we didn't own root_wig at the first place */
/*     } */
/* }; */

}
